package com.neusoft.bizcore.activiti.diagram;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.font.FontRenderContext;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
import java.util.ArrayList;
import java.util.List;

import javax.imageio.ImageIO;

import org.activiti.bpmn.model.AssociationDirection;
import org.activiti.bpmn.model.GraphicInfo;
import org.activiti.engine.ActivitiException;
import org.activiti.image.exception.ActivitiImageException;
import org.activiti.image.util.ReflectUtil;
import org.apache.commons.lang3.StringUtils;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class CustomProcessDiagramCanvas {

    public enum SHAPE_TYPE {
        Rectangle, Rhombus, Ellipse
    }

    protected static final int ARROW_WIDTH = 5;
    protected static final int CONDITIONAL_INDICATOR_WIDTH = 16;
    protected static final int DEFAULT_INDICATOR_WIDTH = 10;
    protected static final int MARKER_WIDTH = 12;
    protected static final int FONT_SIZE = 12;
    protected static final int FONT_SPACING = 2;
    protected static final int TEXT_PADDING = 3;
    protected static final int ANNOTATION_TEXT_PADDING = 7;
    protected static final int LINE_HEIGHT =
            CustomProcessDiagramCanvas.FONT_SIZE + CustomProcessDiagramCanvas.FONT_SPACING;

    /**
     * Colors
     */
    protected static Color TASK_BOX_COLOR = new Color(249, 249, 249);
    protected static Color SUBPROCESS_BOX_COLOR = new Color(255, 255, 255);
    protected static Color EVENT_COLOR = new Color(255, 255, 255);
    protected static Color CONNECTION_COLOR = new Color(88, 88, 88);
    protected static Color CONDITIONAL_INDICATOR_COLOR = new Color(255, 255, 255);
    protected static Color RUNNING_HIGHLIGHT_COLOR = Color.RED;
    protected static Color HIGHLIGHT_COLOR = Color.GREEN;
    protected static Color LABEL_COLOR = Color.black;
    //  protected static Color          LABEL_COLOR     new Color(112, 146, 190)                = Color.blue;
    protected static Color TASK_BORDER_COLOR = new Color(187, 187, 187);
    protected static Color EVENT_BORDER_COLOR = new Color(88, 88, 88);
    protected static Color SUBPROCESS_BORDER_COLOR = new Color(0, 0, 0);

    // Fonts
    protected static Font LABEL_FONT = new Font("微软雅黑", Font.ITALIC, 11);
    protected static Font ANNOTATION_FONT = new Font("Arial", Font.PLAIN, CustomProcessDiagramCanvas.FONT_SIZE);
    protected static Font TASK_FONT = new Font("Arial", Font.PLAIN, CustomProcessDiagramCanvas.FONT_SIZE);

    // Strokes
    //TODO 边框宽度修改
    //protected static Stroke THICK_TASK_BORDER_STROKE = new BasicStroke(3.0f);
    protected static Stroke THICK_TASK_BORDER_STROKE = new BasicStroke(2.0f);
    protected static Stroke GATEWAY_TYPE_STROKE = new BasicStroke(3.0f);
    protected static Stroke END_EVENT_STROKE = new BasicStroke(3.0f);
    protected static Stroke MULTI_INSTANCE_STROKE = new BasicStroke(1.3f);
    protected static Stroke EVENT_SUBPROCESS_STROKE =
            new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1.0f, new float[] {1.0f }, 0.0f);
    protected static Stroke NON_INTERRUPTING_EVENT_STROKE =
            new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1.0f, new float[] {4.0f, 3.0f }, 0.0f);
    protected static Stroke HIGHLIGHT_FLOW_STROKE = new BasicStroke(1.3f);
    protected static Stroke ANNOTATION_STROKE = new BasicStroke(2.0f);
    protected static Stroke ASSOCIATION_STROKE =
            new BasicStroke(2.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1.0f, new float[] {2.0f, 2.0f }, 0.0f);

    // icons
    protected static int ICON_PADDING = 5;
    protected static BufferedImage USERTASK_IMAGE;
    protected static BufferedImage SCRIPTTASK_IMAGE;
    protected static BufferedImage SERVICETASK_IMAGE;
    protected static BufferedImage RECEIVETASK_IMAGE;
    protected static BufferedImage SENDTASK_IMAGE;
    protected static BufferedImage MANUALTASK_IMAGE;
    protected static BufferedImage BUSINESS_RULE_TASK_IMAGE;
    protected static BufferedImage SHELL_TASK_IMAGE;
    protected static BufferedImage MULE_TASK_IMAGE;
    protected static BufferedImage CAMEL_TASK_IMAGE;

    protected static BufferedImage TIMER_IMAGE;
    protected static BufferedImage COMPENSATE_THROW_IMAGE;
    protected static BufferedImage COMPENSATE_CATCH_IMAGE;
    protected static BufferedImage ERROR_THROW_IMAGE;
    protected static BufferedImage ERROR_CATCH_IMAGE;
    protected static BufferedImage MESSAGE_THROW_IMAGE;
    protected static BufferedImage MESSAGE_CATCH_IMAGE;
    protected static BufferedImage SIGNAL_CATCH_IMAGE;
    protected static BufferedImage SIGNAL_THROW_IMAGE;

    protected static BufferedImage CANCEL_END_IMAGE;

    protected int canvasWidth = -1;
    protected int canvasHeight = -1;
    protected int minX = -1;
    protected int minY = -1;
    protected BufferedImage processDiagram;
    protected Graphics2D g;
    protected FontMetrics fontMetrics;
    protected boolean closed;
    protected ClassLoader customClassLoader;
    protected String activityFontName = "Arial";
    protected String labelFontName = "Arial";

    /**
     * Creates an empty canvas with given width and height. Allows to specify
     * minimal boundaries on the left and upper side of the canvas. This is
     * useful for diagrams that have white space there. Everything beneath these
     * minimum values will be cropped. It's also possible to pass a specific
     * font name and a class loader for the icon images.
     */
    public CustomProcessDiagramCanvas(final int width, final int height, final int minX, final int minY,
            final String imageType, final String activityFontName, final String labelFontName,
            final ClassLoader customClassLoader) {

        this.canvasWidth = width;
        this.canvasHeight = height;
        this.minX = minX;
        this.minY = minY;
        if (activityFontName != null) {
            this.activityFontName = activityFontName;
        }
        if (labelFontName != null) {
            this.labelFontName = labelFontName;
        }
        this.customClassLoader = customClassLoader;

        this.initialize(imageType);
    }

    /**
     * Creates an empty canvas with given width and height. Allows to specify
     * minimal boundaries on the left and upper side of the canvas. This is
     * useful for diagrams that have white space there (eg Signavio). Everything
     * beneath these minimum values will be cropped.
     *
     * @param minX Hint that will be used when generating the image. Parts that
     *        fall below minX on the horizontal scale will be cropped.
     * @param minY Hint that will be used when generating the image. Parts that
     *        fall below minX on the horizontal scale will be cropped.
     */
    public CustomProcessDiagramCanvas(final int width, final int height, final int minX, final int minY,
            final String imageType) {
        this.canvasWidth = width;
        this.canvasHeight = height;
        this.minX = minX;
        this.minY = minY;

        this.initialize(imageType);
    }

    public void initialize(final String imageType) {
        if ("png".equalsIgnoreCase(imageType)) {
            this.processDiagram = new BufferedImage(this.canvasWidth, this.canvasHeight, BufferedImage.TYPE_INT_ARGB);
        } else {
            this.processDiagram = new BufferedImage(this.canvasWidth, this.canvasHeight, BufferedImage.TYPE_INT_RGB);
        }

        this.g = this.processDiagram.createGraphics();
        if ("png".equalsIgnoreCase(imageType) == false) {
            this.g.setBackground(new Color(255, 255, 255, 0));
            this.g.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
        }

        this.g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        this.g.setPaint(Color.black);

        final Font font = new Font(this.activityFontName, Font.BOLD, CustomProcessDiagramCanvas.FONT_SIZE);
        this.g.setFont(font);
        this.fontMetrics = this.g.getFontMetrics();

        //      LABEL_FONT = new Font(labelFontName, Font.ITALIC, 10);

        try {
            CustomProcessDiagramCanvas.USERTASK_IMAGE =
                    ImageIO.read(ReflectUtil.getResource("org/activiti/icons/userTask.png", this.customClassLoader));
            CustomProcessDiagramCanvas.SCRIPTTASK_IMAGE =
                    ImageIO.read(ReflectUtil.getResource("org/activiti/icons/scriptTask.png", this.customClassLoader));
            CustomProcessDiagramCanvas.SERVICETASK_IMAGE =
                    ImageIO.read(ReflectUtil.getResource("org/activiti/icons/serviceTask.png", this.customClassLoader));
            CustomProcessDiagramCanvas.RECEIVETASK_IMAGE =
                    ImageIO.read(ReflectUtil.getResource("org/activiti/icons/receiveTask.png", this.customClassLoader));
            CustomProcessDiagramCanvas.SENDTASK_IMAGE =
                    ImageIO.read(ReflectUtil.getResource("org/activiti/icons/sendTask.png", this.customClassLoader));
            CustomProcessDiagramCanvas.MANUALTASK_IMAGE =
                    ImageIO.read(ReflectUtil.getResource("org/activiti/icons/manualTask.png", this.customClassLoader));
            CustomProcessDiagramCanvas.BUSINESS_RULE_TASK_IMAGE = ImageIO
                    .read(ReflectUtil.getResource("org/activiti/icons/businessRuleTask.png", this.customClassLoader));
            CustomProcessDiagramCanvas.SHELL_TASK_IMAGE =
                    ImageIO.read(ReflectUtil.getResource("org/activiti/icons/shellTask.png", this.customClassLoader));
            CustomProcessDiagramCanvas.CAMEL_TASK_IMAGE =
                    ImageIO.read(ReflectUtil.getResource("org/activiti/icons/camelTask.png", this.customClassLoader));
            CustomProcessDiagramCanvas.MULE_TASK_IMAGE =
                    ImageIO.read(ReflectUtil.getResource("org/activiti/icons/muleTask.png", this.customClassLoader));

            CustomProcessDiagramCanvas.TIMER_IMAGE =
                    ImageIO.read(ReflectUtil.getResource("org/activiti/icons/timer.png", this.customClassLoader));
            CustomProcessDiagramCanvas.COMPENSATE_THROW_IMAGE = ImageIO
                    .read(ReflectUtil.getResource("org/activiti/icons/compensate-throw.png", this.customClassLoader));
            CustomProcessDiagramCanvas.COMPENSATE_CATCH_IMAGE =
                    ImageIO.read(ReflectUtil.getResource("org/activiti/icons/compensate.png", this.customClassLoader));
            CustomProcessDiagramCanvas.ERROR_THROW_IMAGE =
                    ImageIO.read(ReflectUtil.getResource("org/activiti/icons/error-throw.png", this.customClassLoader));
            CustomProcessDiagramCanvas.ERROR_CATCH_IMAGE =
                    ImageIO.read(ReflectUtil.getResource("org/activiti/icons/error.png", this.customClassLoader));
            CustomProcessDiagramCanvas.MESSAGE_THROW_IMAGE = ImageIO
                    .read(ReflectUtil.getResource("org/activiti/icons/message-throw.png", this.customClassLoader));
            CustomProcessDiagramCanvas.MESSAGE_CATCH_IMAGE =
                    ImageIO.read(ReflectUtil.getResource("org/activiti/icons/message.png", this.customClassLoader));
            CustomProcessDiagramCanvas.SIGNAL_THROW_IMAGE = ImageIO
                    .read(ReflectUtil.getResource("org/activiti/icons/signal-throw.png", this.customClassLoader));
            CustomProcessDiagramCanvas.SIGNAL_CATCH_IMAGE =
                    ImageIO.read(ReflectUtil.getResource("org/activiti/icons/signal.png", this.customClassLoader));

            CustomProcessDiagramCanvas.CANCEL_END_IMAGE =
                    ImageIO.read(ReflectUtil.getResource("org/activiti/icons/cancel-end.png", this.customClassLoader));
        } catch (final IOException e) {
            CustomProcessDiagramCanvas.log.warn("Could not load image for process diagram creation: {}",
                    e.getMessage());
        }
    }

    /**
     * Generates an image of what currently is drawn on the canvas. Throws an
     * {@link ActivitiException} when {@link #close()} is already called.
     */
    public InputStream generateImage(final String imageType) {
        if (this.closed) {
            throw new ActivitiImageException("ProcessDiagramGenerator already closed");
        }

        final ByteArrayOutputStream out = new ByteArrayOutputStream();
        try {
            // Try to remove white space
            this.minX = (this.minX <= 5) ? 5 : this.minX;
            this.minY = (this.minY <= 5) ? 5 : this.minY;
            BufferedImage imageToSerialize = this.processDiagram;
            if ((this.minX >= 0) && (this.minY >= 0)) {
                imageToSerialize = this.processDiagram.getSubimage(this.minX - 5, this.minY - 5,
                        (this.canvasWidth - this.minX) + 5, (this.canvasHeight - this.minY) + 5);
            }
            ImageIO.write(imageToSerialize, imageType, out);

        } catch (final IOException e) {
            throw new ActivitiImageException("Error while generating process image", e);
        } finally {
            try {
                if (out != null) {
                    out.close();
                }
            } catch (final IOException ignore) {
                // Exception is silently ignored
            }
        }
        return new ByteArrayInputStream(out.toByteArray());
    }

    /**
     * Generates an image of what currently is drawn on the canvas. Throws an
     * {@link ActivitiException} when {@link #close()} is already called.
     */
    public BufferedImage generateBufferedImage(final String imageType) {
        if (this.closed) {
            throw new ActivitiImageException("ProcessDiagramGenerator already closed");
        }

        // Try to remove white space
        this.minX = (this.minX <= 5) ? 5 : this.minX;
        this.minY = (this.minY <= 5) ? 5 : this.minY;
        BufferedImage imageToSerialize = this.processDiagram;
        if ((this.minX >= 0) && (this.minY >= 0)) {
            imageToSerialize = this.processDiagram.getSubimage(this.minX - 5, this.minY - 5,
                    (this.canvasWidth - this.minX) + 5, (this.canvasHeight - this.minY) + 5);
        }
        return imageToSerialize;
    }

    /**
     * Closes the canvas which dissallows further drawing and releases graphical
     * resources.
     */
    public void close() {
        this.g.dispose();
        this.closed = true;
    }

    public void drawNoneStartEvent(final String name, final GraphicInfo graphicInfo) {
        this.drawStartEvent(name, graphicInfo, null, 1.0);
    }

    public void drawTimerStartEvent(final String name, final GraphicInfo graphicInfo, final double scaleFactor) {
        this.drawStartEvent(name, graphicInfo, CustomProcessDiagramCanvas.TIMER_IMAGE, scaleFactor);
    }

    public void drawSignalStartEvent(final String name, final GraphicInfo graphicInfo, final double scaleFactor) {
        this.drawStartEvent(name, graphicInfo, CustomProcessDiagramCanvas.SIGNAL_CATCH_IMAGE, scaleFactor);
    }

    public void drawMessageStartEvent(final GraphicInfo graphicInfo, final double scaleFactor) {
        this.drawStartEvent(null, graphicInfo, CustomProcessDiagramCanvas.MESSAGE_CATCH_IMAGE, scaleFactor);
    }

    public void drawStartEvent(final String name, final GraphicInfo graphicInfo, final BufferedImage image,
            final double scaleFactor) {
        final Paint originalPaint = this.g.getPaint();
        this.g.setPaint(CustomProcessDiagramCanvas.EVENT_COLOR);
        final Ellipse2D circle = new Ellipse2D.Double(graphicInfo.getX(), graphicInfo.getY(), graphicInfo.getWidth(),
                graphicInfo.getHeight());
        this.g.fill(circle);
        this.g.setPaint(CustomProcessDiagramCanvas.EVENT_BORDER_COLOR);
        this.g.draw(circle);
        this.g.setPaint(originalPaint);
        if (image != null) {
            // calculate coordinates to center image
            final int imageX = (int) Math.round(
                    (graphicInfo.getX() + (graphicInfo.getWidth() / 2)) - ((image.getWidth() / 2) * scaleFactor));
            final int imageY = (int) Math.round(
                    (graphicInfo.getY() + (graphicInfo.getHeight() / 2)) - ((image.getHeight() / 2) * scaleFactor));
            this.g.drawImage(image, imageX, imageY, (int) (image.getWidth() / scaleFactor),
                    (int) (image.getHeight() / scaleFactor), null);
        }

        // text
        if (StringUtils.isNotEmpty(name)) {
            final int x = (int) graphicInfo.getX();
            final int y = (int) graphicInfo.getY();
            final int width = (int) graphicInfo.getWidth();
            final int height = (int) graphicInfo.getHeight();
            final int boxWidth = width - (2 * CustomProcessDiagramCanvas.TEXT_PADDING);
            final int boxHeight = height - CustomProcessDiagramCanvas.MARKER_WIDTH;
            final int boxX = (x + (width / 2)) - (boxWidth / 2);
            final int boxY = ((y + (height / 2)) - (boxHeight / 2));

            this.drawMultilineCentredText(name, boxX, boxY, boxWidth, boxHeight);
        }

    }

    public void drawNoneEndEvent(final String name, final GraphicInfo graphicInfo, final double scaleFactor) {
        final Paint originalPaint = this.g.getPaint();
        final Stroke originalStroke = this.g.getStroke();
        this.g.setPaint(CustomProcessDiagramCanvas.EVENT_COLOR);
        final Ellipse2D circle = new Ellipse2D.Double(graphicInfo.getX(), graphicInfo.getY(), graphicInfo.getWidth(),
                graphicInfo.getHeight());
        this.g.fill(circle);
        this.g.setPaint(CustomProcessDiagramCanvas.EVENT_BORDER_COLOR);
        if (scaleFactor == 1.0) {
            this.g.setStroke(CustomProcessDiagramCanvas.END_EVENT_STROKE);
        } else {
            this.g.setStroke(new BasicStroke(2.0f));
        }
        this.g.draw(circle);
        this.g.setStroke(originalStroke);
        this.g.setPaint(originalPaint);

        // text
        if (StringUtils.isNotEmpty(name)) {
            final int x = (int) graphicInfo.getX();
            final int y = (int) graphicInfo.getY();
            final int width = (int) graphicInfo.getWidth();
            final int height = (int) graphicInfo.getHeight();
            final int boxWidth = width - (2 * CustomProcessDiagramCanvas.TEXT_PADDING);
            final int boxHeight = height - CustomProcessDiagramCanvas.MARKER_WIDTH;
            final int boxX = (x + (width / 2)) - (boxWidth / 2);
            final int boxY = ((y + (height / 2)) - (boxHeight / 2));

            this.drawMultilineCentredText(name, boxX, boxY, boxWidth, boxHeight);
        }

    }

    public void drawErrorEndEvent(final String name, final GraphicInfo graphicInfo, final double scaleFactor) {
        this.drawErrorEndEvent(graphicInfo, scaleFactor);
        if (scaleFactor == 1.0) {
            this.drawLabel(name, graphicInfo);
        }
    }

    public void drawErrorEndEvent(final GraphicInfo graphicInfo, final double scaleFactor) {
        this.drawNoneEndEvent(null, graphicInfo, scaleFactor);
        this.g.drawImage(CustomProcessDiagramCanvas.ERROR_THROW_IMAGE,
                (int) (graphicInfo.getX() + (graphicInfo.getWidth() / 4)),
                (int) (graphicInfo.getY() + (graphicInfo.getHeight() / 4)),
                (int) (CustomProcessDiagramCanvas.ERROR_THROW_IMAGE.getWidth() / scaleFactor),
                (int) (CustomProcessDiagramCanvas.ERROR_THROW_IMAGE.getHeight() / scaleFactor), null);
    }

    public void drawErrorStartEvent(final GraphicInfo graphicInfo, final double scaleFactor) {
        this.drawNoneStartEvent(null, graphicInfo);
        this.g.drawImage(CustomProcessDiagramCanvas.ERROR_CATCH_IMAGE,
                (int) (graphicInfo.getX() + (graphicInfo.getWidth() / 4)),
                (int) (graphicInfo.getY() + (graphicInfo.getHeight() / 4)),
                (int) (CustomProcessDiagramCanvas.ERROR_CATCH_IMAGE.getWidth() / scaleFactor),
                (int) (CustomProcessDiagramCanvas.ERROR_CATCH_IMAGE.getHeight() / scaleFactor), null);
    }

    public void drawCatchingEvent(final GraphicInfo graphicInfo, final boolean isInterrupting,
            final BufferedImage image, final String eventType, final double scaleFactor) {

        // event circles
        final Ellipse2D outerCircle = new Ellipse2D.Double(graphicInfo.getX(), graphicInfo.getY(),
                graphicInfo.getWidth(), graphicInfo.getHeight());
        int innerCircleSize = (int) (4 / scaleFactor);
        if (innerCircleSize == 0) {
            innerCircleSize = 1;
        }
        final int innerCircleX = (int) graphicInfo.getX() + innerCircleSize;
        final int innerCircleY = (int) graphicInfo.getY() + innerCircleSize;
        final int innerCircleWidth = (int) graphicInfo.getWidth() - (2 * innerCircleSize);
        final int innerCircleHeight = (int) graphicInfo.getHeight() - (2 * innerCircleSize);
        final Ellipse2D innerCircle =
                new Ellipse2D.Double(innerCircleX, innerCircleY, innerCircleWidth, innerCircleHeight);

        final Paint originalPaint = this.g.getPaint();
        final Stroke originalStroke = this.g.getStroke();
        this.g.setPaint(CustomProcessDiagramCanvas.EVENT_COLOR);
        this.g.fill(outerCircle);

        this.g.setPaint(CustomProcessDiagramCanvas.EVENT_BORDER_COLOR);
        if (isInterrupting == false) {
            this.g.setStroke(CustomProcessDiagramCanvas.NON_INTERRUPTING_EVENT_STROKE);
        }
        this.g.draw(outerCircle);
        this.g.setStroke(originalStroke);
        this.g.setPaint(originalPaint);
        this.g.draw(innerCircle);

        if (image != null) {
            // calculate coordinates to center image
            int imageX = (int) ((graphicInfo.getX() + (graphicInfo.getWidth() / 2))
                    - ((image.getWidth() / 2) * scaleFactor));
            int imageY = (int) ((graphicInfo.getY() + (graphicInfo.getHeight() / 2))
                    - ((image.getHeight() / 2) * scaleFactor));
            if ((scaleFactor == 1.0) && "timer".equals(eventType)) {
                // move image one pixel to center timer image
                imageX++;
                imageY++;
            }
            this.g.drawImage(image, imageX, imageY, (int) (image.getWidth() / scaleFactor),
                    (int) (image.getHeight() / scaleFactor), null);
        }
    }

    public void drawCatchingCompensateEvent(final String name, final GraphicInfo graphicInfo,
            final boolean isInterrupting, final double scaleFactor) {
        this.drawCatchingCompensateEvent(graphicInfo, isInterrupting, scaleFactor);
        this.drawLabel(name, graphicInfo);
    }

    public void drawCatchingCompensateEvent(final GraphicInfo graphicInfo, final boolean isInterrupting,
            final double scaleFactor) {
        this.drawCatchingEvent(graphicInfo, isInterrupting, CustomProcessDiagramCanvas.COMPENSATE_CATCH_IMAGE,
                "compensate", scaleFactor);
    }

    public void drawCatchingTimerEvent(final String name, final GraphicInfo graphicInfo, final boolean isInterrupting,
            final double scaleFactor) {
        this.drawCatchingTimerEvent(graphicInfo, isInterrupting, scaleFactor);
        this.drawLabel(name, graphicInfo);
    }

    public void drawCatchingTimerEvent(final GraphicInfo graphicInfo, final boolean isInterrupting,
            final double scaleFactor) {
        this.drawCatchingEvent(graphicInfo, isInterrupting, CustomProcessDiagramCanvas.TIMER_IMAGE, "timer",
                scaleFactor);
    }

    public void drawCatchingErrorEvent(final String name, final GraphicInfo graphicInfo, final boolean isInterrupting,
            final double scaleFactor) {
        this.drawCatchingErrorEvent(graphicInfo, isInterrupting, scaleFactor);
        this.drawLabel(name, graphicInfo);
    }

    public void drawCatchingErrorEvent(final GraphicInfo graphicInfo, final boolean isInterrupting,
            final double scaleFactor) {
        this.drawCatchingEvent(graphicInfo, isInterrupting, CustomProcessDiagramCanvas.ERROR_CATCH_IMAGE, "error",
                scaleFactor);
    }

    public void drawCatchingSignalEvent(final String name, final GraphicInfo graphicInfo, final boolean isInterrupting,
            final double scaleFactor) {
        this.drawCatchingSignalEvent(graphicInfo, isInterrupting, scaleFactor);
        this.drawLabel(name, graphicInfo);
    }

    public void drawCatchingSignalEvent(final GraphicInfo graphicInfo, final boolean isInterrupting,
            final double scaleFactor) {
        this.drawCatchingEvent(graphicInfo, isInterrupting, CustomProcessDiagramCanvas.SIGNAL_CATCH_IMAGE, "signal",
                scaleFactor);
    }

    public void drawCatchingMessageEvent(final GraphicInfo graphicInfo, final boolean isInterrupting,
            final double scaleFactor) {
        this.drawCatchingEvent(graphicInfo, isInterrupting, CustomProcessDiagramCanvas.MESSAGE_CATCH_IMAGE, "message",
                scaleFactor);
    }

    public void drawCatchingMessageEvent(final String name, final GraphicInfo graphicInfo, final boolean isInterrupting,
            final double scaleFactor) {
        this.drawCatchingEvent(graphicInfo, isInterrupting, CustomProcessDiagramCanvas.MESSAGE_CATCH_IMAGE, "message",
                scaleFactor);
        this.drawLabel(name, graphicInfo);
    }

    public void drawThrowingCompensateEvent(final GraphicInfo graphicInfo, final double scaleFactor) {
        this.drawCatchingEvent(graphicInfo, true, CustomProcessDiagramCanvas.COMPENSATE_THROW_IMAGE, "compensate",
                scaleFactor);
    }

    public void drawThrowingSignalEvent(final GraphicInfo graphicInfo, final double scaleFactor) {
        this.drawCatchingEvent(graphicInfo, true, CustomProcessDiagramCanvas.SIGNAL_THROW_IMAGE, "signal", scaleFactor);
    }

    public void drawThrowingNoneEvent(final GraphicInfo graphicInfo, final double scaleFactor) {
        this.drawCatchingEvent(graphicInfo, true, null, "none", scaleFactor);
    }

    public void drawSequenceflow(final int srcX, final int srcY, final int targetX, final int targetY,
            final boolean conditional, final double scaleFactor) {
        this.drawSequenceflow(srcX, srcY, targetX, targetY, conditional, false, scaleFactor);
    }

    public void drawSequenceflow(final int srcX, final int srcY, final int targetX, final int targetY,
            final boolean conditional, final boolean highLighted, final double scaleFactor) {
        final Paint originalPaint = this.g.getPaint();
        if (highLighted) {
            this.g.setPaint(CustomProcessDiagramCanvas.HIGHLIGHT_COLOR);
        }
        final Line2D.Double line = new Line2D.Double(srcX, srcY, targetX, targetY);
        this.g.draw(line);
        this.drawArrowHead(line, scaleFactor);

        if (conditional) {
            this.drawConditionalSequenceFlowIndicator(line, scaleFactor);
        }

        if (highLighted) {
            this.g.setPaint(originalPaint);
        }
    }

    public void drawAssociation(final int[] xPoints, final int[] yPoints,
            final AssociationDirection associationDirection, final boolean highLighted, final double scaleFactor) {
        final boolean conditional = false, isDefault = false;
        this.drawConnection(xPoints, yPoints, conditional, isDefault, "association", associationDirection, highLighted,
                scaleFactor);
    }

    public void drawSequenceflow(final int[] xPoints, final int[] yPoints, final boolean conditional,
            final boolean isDefault, final boolean highLighted, final double scaleFactor) {
        this.drawConnection(xPoints, yPoints, conditional, isDefault, "sequenceFlow", AssociationDirection.ONE,
                highLighted, scaleFactor);
    }

    public void drawConnection(final int[] xPoints, final int[] yPoints, final boolean conditional,
            final boolean isDefault, final String connectionType, final AssociationDirection associationDirection,
            final boolean highLighted, final double scaleFactor) {

        final Paint originalPaint = this.g.getPaint();
        final Stroke originalStroke = this.g.getStroke();

        this.g.setPaint(CustomProcessDiagramCanvas.CONNECTION_COLOR);
        if ("association".equals(connectionType)) {
            this.g.setStroke(CustomProcessDiagramCanvas.ASSOCIATION_STROKE);
        } else if (highLighted) {
            this.g.setPaint(CustomProcessDiagramCanvas.HIGHLIGHT_COLOR);
            this.g.setStroke(CustomProcessDiagramCanvas.HIGHLIGHT_FLOW_STROKE);
        }

        for (int i = 1; i < xPoints.length; i++) {
            final Integer sourceX = xPoints[i - 1];
            final Integer sourceY = yPoints[i - 1];
            final Integer targetX = xPoints[i];
            final Integer targetY = yPoints[i];
            final Line2D.Double line = new Line2D.Double(sourceX, sourceY, targetX, targetY);
            this.g.draw(line);
        }

        if (isDefault) {
            final Line2D.Double line = new Line2D.Double(xPoints[0], yPoints[0], xPoints[1], yPoints[1]);
            this.drawDefaultSequenceFlowIndicator(line, scaleFactor);
        }

        if (conditional) {
            final Line2D.Double line = new Line2D.Double(xPoints[0], yPoints[0], xPoints[1], yPoints[1]);
            this.drawConditionalSequenceFlowIndicator(line, scaleFactor);
        }

        if (associationDirection.equals(AssociationDirection.ONE)
                || associationDirection.equals(AssociationDirection.BOTH)) {
            final Line2D.Double line = new Line2D.Double(xPoints[xPoints.length - 2], yPoints[xPoints.length - 2],
                    xPoints[xPoints.length - 1], yPoints[xPoints.length - 1]);
            this.drawArrowHead(line, scaleFactor);
        }
        if (associationDirection.equals(AssociationDirection.BOTH)) {
            final Line2D.Double line = new Line2D.Double(xPoints[1], yPoints[1], xPoints[0], yPoints[0]);
            this.drawArrowHead(line, scaleFactor);
        }
        this.g.setPaint(originalPaint);
        this.g.setStroke(originalStroke);
    }

    public void drawSequenceflowWithoutArrow(final int srcX, final int srcY, final int targetX, final int targetY,
            final boolean conditional, final double scaleFactor) {
        this.drawSequenceflowWithoutArrow(srcX, srcY, targetX, targetY, conditional, false, scaleFactor);
    }

    public void drawSequenceflowWithoutArrow(final int srcX, final int srcY, final int targetX, final int targetY,
            final boolean conditional, final boolean highLighted, final double scaleFactor) {
        final Paint originalPaint = this.g.getPaint();
        if (highLighted) {
            this.g.setPaint(CustomProcessDiagramCanvas.HIGHLIGHT_COLOR);
        }

        final Line2D.Double line = new Line2D.Double(srcX, srcY, targetX, targetY);
        this.g.draw(line);

        if (conditional) {
            this.drawConditionalSequenceFlowIndicator(line, scaleFactor);
        }

        if (highLighted) {
            this.g.setPaint(originalPaint);
        }
    }

    public void drawArrowHead(final Line2D.Double line, final double scaleFactor) {
        int doubleArrowWidth = (int) ((2 * CustomProcessDiagramCanvas.ARROW_WIDTH) / scaleFactor);
        if (doubleArrowWidth == 0) {
            doubleArrowWidth = 2;
        }
        final Polygon arrowHead = new Polygon();
        arrowHead.addPoint(0, 0);
        int arrowHeadPoint = (int) (-CustomProcessDiagramCanvas.ARROW_WIDTH / scaleFactor);
        if (arrowHeadPoint == 0) {
            arrowHeadPoint = -1;
        }
        arrowHead.addPoint(arrowHeadPoint, -doubleArrowWidth);
        arrowHeadPoint = (int) (CustomProcessDiagramCanvas.ARROW_WIDTH / scaleFactor);
        if (arrowHeadPoint == 0) {
            arrowHeadPoint = 1;
        }
        arrowHead.addPoint(arrowHeadPoint, -doubleArrowWidth);

        final AffineTransform transformation = new AffineTransform();
        transformation.setToIdentity();
        final double angle = Math.atan2(line.y2 - line.y1, line.x2 - line.x1);
        transformation.translate(line.x2, line.y2);
        transformation.rotate((angle - (Math.PI / 2d)));

        final AffineTransform originalTransformation = this.g.getTransform();
        this.g.setTransform(transformation);
        this.g.fill(arrowHead);
        this.g.setTransform(originalTransformation);
    }

    public void drawDefaultSequenceFlowIndicator(final Line2D.Double line, final double scaleFactor) {
        final double length = CustomProcessDiagramCanvas.DEFAULT_INDICATOR_WIDTH / scaleFactor,
                halfOfLength = length / 2, f = 8;
        final Line2D.Double defaultIndicator = new Line2D.Double(-halfOfLength, 0, halfOfLength, 0);

        final double angle = Math.atan2(line.y2 - line.y1, line.x2 - line.x1);
        final double dx = f * Math.cos(angle), dy = f * Math.sin(angle), x1 = line.x1 + dx, y1 = line.y1 + dy;

        final AffineTransform transformation = new AffineTransform();
        transformation.setToIdentity();
        transformation.translate(x1, y1);
        transformation.rotate((angle - ((3 * Math.PI) / 4)));

        final AffineTransform originalTransformation = this.g.getTransform();
        this.g.setTransform(transformation);
        this.g.draw(defaultIndicator);

        this.g.setTransform(originalTransformation);
    }

    public void drawConditionalSequenceFlowIndicator(final Line2D.Double line, final double scaleFactor) {
        if (scaleFactor > 1.0) {
            return;
        }
        final int horizontal = (int) (CustomProcessDiagramCanvas.CONDITIONAL_INDICATOR_WIDTH * 0.7);
        final int halfOfHorizontal = horizontal / 2;
        final int halfOfVertical = CustomProcessDiagramCanvas.CONDITIONAL_INDICATOR_WIDTH / 2;

        final Polygon conditionalIndicator = new Polygon();
        conditionalIndicator.addPoint(0, 0);
        conditionalIndicator.addPoint(-halfOfHorizontal, halfOfVertical);
        conditionalIndicator.addPoint(0, CustomProcessDiagramCanvas.CONDITIONAL_INDICATOR_WIDTH);
        conditionalIndicator.addPoint(halfOfHorizontal, halfOfVertical);

        final AffineTransform transformation = new AffineTransform();
        transformation.setToIdentity();
        final double angle = Math.atan2(line.y2 - line.y1, line.x2 - line.x1);
        transformation.translate(line.x1, line.y1);
        transformation.rotate((angle - (Math.PI / 2d)));

        final AffineTransform originalTransformation = this.g.getTransform();
        this.g.setTransform(transformation);
        this.g.draw(conditionalIndicator);

        final Paint originalPaint = this.g.getPaint();
        this.g.setPaint(CustomProcessDiagramCanvas.CONDITIONAL_INDICATOR_COLOR);
        this.g.fill(conditionalIndicator);

        this.g.setPaint(originalPaint);
        this.g.setTransform(originalTransformation);
    }

    public void drawTask(final BufferedImage icon, final String name, final GraphicInfo graphicInfo,
            final double scaleFactor) {
        this.drawTask(name, graphicInfo);
        this.g.drawImage(icon, (int) (graphicInfo.getX() + (CustomProcessDiagramCanvas.ICON_PADDING / scaleFactor)),
                (int) (graphicInfo.getY() + (CustomProcessDiagramCanvas.ICON_PADDING / scaleFactor)),
                (int) (icon.getWidth() / scaleFactor),
                (int) (icon.getHeight() / scaleFactor), null);
    }

    public void drawTask(final String name, final GraphicInfo graphicInfo) {
        this.drawTask(name, graphicInfo, false);
    }

    public void drawPoolOrLane(final String name, final GraphicInfo graphicInfo) {
        final int x = (int) graphicInfo.getX();
        final int y = (int) graphicInfo.getY();
        final int width = (int) graphicInfo.getWidth();
        final int height = (int) graphicInfo.getHeight();
        this.g.drawRect(x, y, width, height);

        // Add the name as text, vertical
        if ((name != null) && (name.length() > 0)) {
            // Include some padding
            final int availableTextSpace = height - 6;

            // Create rotation for derived font
            final AffineTransform transformation = new AffineTransform();
            transformation.setToIdentity();
            transformation.rotate((270 * Math.PI) / 180);

            final Font currentFont = this.g.getFont();
            final Font theDerivedFont = currentFont.deriveFont(transformation);
            this.g.setFont(theDerivedFont);

            final String truncated = this.fitTextToWidth(name, availableTextSpace);
            final int realWidth = this.fontMetrics.stringWidth(truncated);

            this.g.drawString(truncated, x + 2 + this.fontMetrics.getHeight(),
                    (3 + y + availableTextSpace) - ((availableTextSpace - realWidth) / 2));
            this.g.setFont(currentFont);
        }
    }

    protected void drawTask(final String name, final GraphicInfo graphicInfo, final boolean thickBorder) {
        final Paint originalPaint = this.g.getPaint();
        final int x = (int) graphicInfo.getX();
        final int y = (int) graphicInfo.getY();
        final int width = (int) graphicInfo.getWidth();
        final int height = (int) graphicInfo.getHeight();

        // Create a new gradient paint for every task box, gradient depends on x and y and is not relative
        this.g.setPaint(CustomProcessDiagramCanvas.TASK_BOX_COLOR);

        int arcR = 6;
        if (thickBorder) {
            arcR = 3;
        }
        // shape
        final RoundRectangle2D rect = new RoundRectangle2D.Double(x, y, width, height, arcR, arcR);
        this.g.fill(rect);
        this.g.setPaint(CustomProcessDiagramCanvas.TASK_BORDER_COLOR);

        if (thickBorder) {
            final Stroke originalStroke = this.g.getStroke();
            this.g.setStroke(CustomProcessDiagramCanvas.THICK_TASK_BORDER_STROKE);
            this.g.draw(rect);
            this.g.setStroke(originalStroke);
        } else {
            this.g.draw(rect);
        }

        this.g.setPaint(originalPaint);
        // text
        if ((name != null) && (name.length() > 0)) {
            final int boxWidth = width - (2 * CustomProcessDiagramCanvas.TEXT_PADDING);
            final int boxHeight = height - 16 - CustomProcessDiagramCanvas.ICON_PADDING
                    - CustomProcessDiagramCanvas.ICON_PADDING - CustomProcessDiagramCanvas.MARKER_WIDTH - 2 - 2;
            final int boxX = (x + (width / 2)) - (boxWidth / 2);
            final int boxY = (((y + (height / 2)) - (boxHeight / 2)) + CustomProcessDiagramCanvas.ICON_PADDING
                    + CustomProcessDiagramCanvas.ICON_PADDING) - 2 - 2;

            this.drawMultilineCentredText(name, boxX, boxY, boxWidth, boxHeight);
        }
    }

    protected void drawMultilineCentredText(final String text, final int x, final int y, final int boxWidth,
            final int boxHeight) {
        this.drawMultilineText(text, x, y, boxWidth, boxHeight, true);
    }

    protected void drawMultilineAnnotationText(final String text, final int x, final int y, final int boxWidth,
            final int boxHeight) {
        this.drawMultilineText(text, x, y, boxWidth, boxHeight, false);
    }

    protected void drawMultilineText(final String text, final int x, final int y, final int boxWidth,
            final int boxHeight, final boolean centered) {
        // Create an attributed string based in input text
        final AttributedString attributedString = new AttributedString(text);
        attributedString.addAttribute(TextAttribute.FONT, this.g.getFont());
        attributedString.addAttribute(TextAttribute.FOREGROUND, Color.black);

        final AttributedCharacterIterator characterIterator = attributedString.getIterator();

        int currentHeight = 0;
        // Prepare a list of lines of text we'll be drawing
        final List<TextLayout> layouts = new ArrayList<>();
        String lastLine = null;

        final LineBreakMeasurer measurer = new LineBreakMeasurer(characterIterator, this.g.getFontRenderContext());

        TextLayout layout = null;
        while ((measurer.getPosition() < characterIterator.getEndIndex()) && (currentHeight <= boxHeight)) {

            final int previousPosition = measurer.getPosition();

            // Request next layout
            layout = measurer.nextLayout(boxWidth);

            final int height = ((Float) (layout.getDescent() + layout.getAscent() + layout.getLeading())).intValue();

            if ((currentHeight + height) > boxHeight) {
                // The line we're about to add should NOT be added anymore, append three dots to previous one instead
                // to indicate more text is truncated
                if (!layouts.isEmpty()) {
                    layouts.remove(layouts.size() - 1);

                    if (lastLine.length() >= 4) {
                        lastLine = lastLine.substring(0, lastLine.length() - 4) + "...";
                    }
                    layouts.add(new TextLayout(lastLine, this.g.getFont(), this.g.getFontRenderContext()));
                }
            } else {
                layouts.add(layout);
                lastLine = text.substring(previousPosition, measurer.getPosition());
                currentHeight += height;
            }
        }

        int currentY = y + (centered ? ((boxHeight - currentHeight) / 2) : 0);
        int currentX = 0;

        // Actually draw the lines
        for (final TextLayout textLayout : layouts) {

            currentY += textLayout.getAscent();
            currentX = x + (centered ? ((boxWidth - ((Double) textLayout.getBounds().getWidth()).intValue()) / 2) : 0);

            textLayout.draw(this.g, currentX, currentY);
            currentY += textLayout.getDescent() + textLayout.getLeading();
        }

    }

    protected String fitTextToWidth(final String original, final int width) {
        String text = original;

        // remove length for "..."
        final int maxWidth = width - 10;

        while ((this.fontMetrics.stringWidth(text + "...") > maxWidth) && (text.length() > 0)) {
            text = text.substring(0, text.length() - 1);
        }

        if (!text.equals(original)) {
            text = text + "...";
        }

        return text;
    }

    public void drawUserTask(final String name, final GraphicInfo graphicInfo, final double scaleFactor) {
        this.drawTask(CustomProcessDiagramCanvas.USERTASK_IMAGE, name, graphicInfo, scaleFactor);
    }

    public void drawScriptTask(final String name, final GraphicInfo graphicInfo, final double scaleFactor) {
        this.drawTask(CustomProcessDiagramCanvas.SCRIPTTASK_IMAGE, name, graphicInfo, scaleFactor);
    }

    public void drawServiceTask(final String name, final GraphicInfo graphicInfo, final double scaleFactor) {
        this.drawTask(CustomProcessDiagramCanvas.SERVICETASK_IMAGE, name, graphicInfo, scaleFactor);
    }

    public void drawReceiveTask(final String name, final GraphicInfo graphicInfo, final double scaleFactor) {
        this.drawTask(CustomProcessDiagramCanvas.RECEIVETASK_IMAGE, name, graphicInfo, scaleFactor);
    }

    public void drawSendTask(final String name, final GraphicInfo graphicInfo, final double scaleFactor) {
        this.drawTask(CustomProcessDiagramCanvas.SENDTASK_IMAGE, name, graphicInfo, scaleFactor);
    }

    public void drawManualTask(final String name, final GraphicInfo graphicInfo, final double scaleFactor) {
        this.drawTask(CustomProcessDiagramCanvas.MANUALTASK_IMAGE, name, graphicInfo, scaleFactor);
    }

    public void drawBusinessRuleTask(final String name, final GraphicInfo graphicInfo, final double scaleFactor) {
        this.drawTask(CustomProcessDiagramCanvas.BUSINESS_RULE_TASK_IMAGE, name, graphicInfo, scaleFactor);
    }

    public void drawCamelTask(final String name, final GraphicInfo graphicInfo, final double scaleFactor) {
        this.drawTask(CustomProcessDiagramCanvas.CAMEL_TASK_IMAGE, name, graphicInfo, scaleFactor);
    }

    public void drawMuleTask(final String name, final GraphicInfo graphicInfo, final double scaleFactor) {
        this.drawTask(CustomProcessDiagramCanvas.MULE_TASK_IMAGE, name, graphicInfo, scaleFactor);
    }

    public void drawExpandedSubProcess(final String name, final GraphicInfo graphicInfo,
            final Boolean isTriggeredByEvent, final double scaleFactor) {
        final RoundRectangle2D rect = new RoundRectangle2D.Double(graphicInfo.getX(), graphicInfo.getY(),
                graphicInfo.getWidth(), graphicInfo.getHeight(), 8, 8);

        // Use different stroke (dashed)
        if (isTriggeredByEvent) {
            final Stroke originalStroke = this.g.getStroke();
            this.g.setStroke(CustomProcessDiagramCanvas.EVENT_SUBPROCESS_STROKE);
            this.g.draw(rect);
            this.g.setStroke(originalStroke);
        } else {
            final Paint originalPaint = this.g.getPaint();
            this.g.setPaint(CustomProcessDiagramCanvas.SUBPROCESS_BOX_COLOR);
            this.g.fill(rect);
            this.g.setPaint(CustomProcessDiagramCanvas.SUBPROCESS_BORDER_COLOR);
            this.g.draw(rect);
            this.g.setPaint(originalPaint);
        }

        if ((scaleFactor == 1.0) && (name != null) && !name.isEmpty()) {
            final String text = this.fitTextToWidth(name, (int) graphicInfo.getWidth());
            this.g.drawString(text, (int) graphicInfo.getX() + 10, (int) graphicInfo.getY() + 15);
        }
    }

    public void drawCollapsedSubProcess(final String name, final GraphicInfo graphicInfo,
            final Boolean isTriggeredByEvent) {
        this.drawCollapsedTask(name, graphicInfo, false);
    }

    public void drawCollapsedCallActivity(final String name, final GraphicInfo graphicInfo) {
        this.drawCollapsedTask(name, graphicInfo, true);
    }

    protected void drawCollapsedTask(final String name, final GraphicInfo graphicInfo, final boolean thickBorder) {
        // The collapsed marker is now visualized separately
        this.drawTask(name, graphicInfo, thickBorder);
    }

    public void drawCollapsedMarker(final int x, final int y, final int width, final int height) {
        // rectangle
        final int rectangleWidth = CustomProcessDiagramCanvas.MARKER_WIDTH;
        final int rectangleHeight = CustomProcessDiagramCanvas.MARKER_WIDTH;
        final Rectangle rect = new Rectangle(x + ((width - rectangleWidth) / 2), (y + height) - rectangleHeight - 3,
                rectangleWidth, rectangleHeight);
        this.g.draw(rect);

        // plus inside rectangle
        Line2D.Double line =
                new Line2D.Double(rect.getCenterX(), rect.getY() + 2, rect.getCenterX(), rect.getMaxY() - 2);
        this.g.draw(line);
        line = new Line2D.Double(rect.getMinX() + 2, rect.getCenterY(), rect.getMaxX() - 2, rect.getCenterY());
        this.g.draw(line);
    }

    public void drawActivityMarkers(final int x, final int y, final int width, final int height,
            final boolean multiInstanceSequential, final boolean multiInstanceParallel, final boolean collapsed) {
        if (collapsed) {
            if (!multiInstanceSequential && !multiInstanceParallel) {
                this.drawCollapsedMarker(x, y, width, height);
            } else {
                this.drawCollapsedMarker(x - (CustomProcessDiagramCanvas.MARKER_WIDTH / 2) - 2, y, width, height);
                if (multiInstanceSequential) {
                    this.drawMultiInstanceMarker(true, x + (CustomProcessDiagramCanvas.MARKER_WIDTH / 2) + 2, y, width,
                            height);
                } else {
                    this.drawMultiInstanceMarker(false, x + (CustomProcessDiagramCanvas.MARKER_WIDTH / 2) + 2, y, width,
                            height);
                }
            }
        } else {
            if (multiInstanceSequential) {
                this.drawMultiInstanceMarker(true, x, y, width, height);
            } else if (multiInstanceParallel) {
                this.drawMultiInstanceMarker(false, x, y, width, height);
            }
        }
    }

    public void drawGateway(final GraphicInfo graphicInfo) {
        final Polygon rhombus = new Polygon();
        final int x = (int) graphicInfo.getX();
        final int y = (int) graphicInfo.getY();
        final int width = (int) graphicInfo.getWidth();
        final int height = (int) graphicInfo.getHeight();

        rhombus.addPoint(x, y + (height / 2));
        rhombus.addPoint(x + (width / 2), y + height);
        rhombus.addPoint(x + width, y + (height / 2));
        rhombus.addPoint(x + (width / 2), y);
        this.g.draw(rhombus);
    }

    public void drawParallelGateway(final GraphicInfo graphicInfo, final double scaleFactor) {
        // rhombus
        this.drawGateway(graphicInfo);
        final int x = (int) graphicInfo.getX();
        final int y = (int) graphicInfo.getY();
        final int width = (int) graphicInfo.getWidth();
        final int height = (int) graphicInfo.getHeight();

        if (scaleFactor == 1.0) {
            // plus inside rhombus
            final Stroke orginalStroke = this.g.getStroke();
            this.g.setStroke(CustomProcessDiagramCanvas.GATEWAY_TYPE_STROKE);
            Line2D.Double line = new Line2D.Double(x + 10, y + (height / 2), (x + width) - 10, y + (height / 2)); // horizontal
            this.g.draw(line);
            line = new Line2D.Double(x + (width / 2), (y + height) - 10, x + (width / 2), y + 10); // vertical
            this.g.draw(line);
            this.g.setStroke(orginalStroke);
        }
    }

    public void drawExclusiveGateway(final GraphicInfo graphicInfo, final double scaleFactor) {
        // rhombus
        this.drawGateway(graphicInfo);
        final int x = (int) graphicInfo.getX();
        final int y = (int) graphicInfo.getY();
        final int width = (int) graphicInfo.getWidth();
        final int height = (int) graphicInfo.getHeight();

        final int quarterWidth = width / 4;
        final int quarterHeight = height / 4;

        if (scaleFactor == 1.0) {
            // X inside rhombus
            final Stroke orginalStroke = this.g.getStroke();
            this.g.setStroke(CustomProcessDiagramCanvas.GATEWAY_TYPE_STROKE);
            Line2D.Double line = new Line2D.Double(x + quarterWidth + 3, y + quarterHeight + 3,
                    (x + (3 * quarterWidth)) - 3, (y + (3 * quarterHeight)) - 3);
            this.g.draw(line);
            line = new Line2D.Double(x + quarterWidth + 3, (y + (3 * quarterHeight)) - 3, (x + (3 * quarterWidth)) - 3,
                    y + quarterHeight + 3);
            this.g.draw(line);
            this.g.setStroke(orginalStroke);
        }
    }

    public void drawInclusiveGateway(final GraphicInfo graphicInfo, final double scaleFactor) {
        // rhombus
        this.drawGateway(graphicInfo);
        final int x = (int) graphicInfo.getX();
        final int y = (int) graphicInfo.getY();
        final int width = (int) graphicInfo.getWidth();
        final int height = (int) graphicInfo.getHeight();

        final int diameter = width / 2;

        if (scaleFactor == 1.0) {
            // circle inside rhombus
            final Stroke orginalStroke = this.g.getStroke();
            this.g.setStroke(CustomProcessDiagramCanvas.GATEWAY_TYPE_STROKE);
            final Ellipse2D.Double circle = new Ellipse2D.Double(((width - diameter) / 2) + x,
                    ((height - diameter) / 2) + y, diameter, diameter);
            this.g.draw(circle);
            this.g.setStroke(orginalStroke);
        }
    }

    public void drawEventBasedGateway(final GraphicInfo graphicInfo, final double scaleFactor) {
        // rhombus
        this.drawGateway(graphicInfo);

        if (scaleFactor == 1.0) {
            final int x = (int) graphicInfo.getX();
            final int y = (int) graphicInfo.getY();
            final int width = (int) graphicInfo.getWidth();
            final int height = (int) graphicInfo.getHeight();

            final double scale = .6;

            final GraphicInfo eventInfo = new GraphicInfo();
            eventInfo.setX(x + ((width * (1 - scale)) / 2));
            eventInfo.setY(y + ((height * (1 - scale)) / 2));
            eventInfo.setWidth(width * scale);
            eventInfo.setHeight(height * scale);
            this.drawCatchingEvent(eventInfo, true, null, "eventGateway", scaleFactor);

            final double r = width / 6.;

            // create pentagon (coords with respect to center)
            final int topX = (int) (.95 * r); // top right corner
            final int topY = (int) (-.31 * r);
            final int bottomX = (int) (.59 * r); // bottom right corner
            final int bottomY = (int) (.81 * r);

            final int[] xPoints = new int[] {0, topX, bottomX, -bottomX, -topX };
            final int[] yPoints = new int[] {-(int) r, topY, bottomY, bottomY, topY };
            final Polygon pentagon = new Polygon(xPoints, yPoints, 5);
            pentagon.translate(x + (width / 2), y + (width / 2));

            // draw
            this.g.drawPolygon(pentagon);
        }
    }

    public void drawMultiInstanceMarker(final boolean sequential, final int x, final int y, final int width,
            final int height) {
        final int rectangleWidth = CustomProcessDiagramCanvas.MARKER_WIDTH;
        final int rectangleHeight = CustomProcessDiagramCanvas.MARKER_WIDTH;
        final int lineX = x + ((width - rectangleWidth) / 2);
        final int lineY = (y + height) - rectangleHeight - 3;

        final Stroke orginalStroke = this.g.getStroke();
        this.g.setStroke(CustomProcessDiagramCanvas.MULTI_INSTANCE_STROKE);

        if (sequential) {
            this.g.draw(new Line2D.Double(lineX, lineY, lineX + rectangleWidth, lineY));
            this.g.draw(new Line2D.Double(lineX, lineY + (rectangleHeight / 2), lineX + rectangleWidth,
                    lineY + (rectangleHeight / 2)));
            this.g.draw(
                    new Line2D.Double(lineX, lineY + rectangleHeight, lineX + rectangleWidth, lineY + rectangleHeight));
        } else {
            this.g.draw(new Line2D.Double(lineX, lineY, lineX, lineY + rectangleHeight));
            this.g.draw(new Line2D.Double(lineX + (rectangleWidth / 2), lineY, lineX + (rectangleWidth / 2),
                    lineY + rectangleHeight));
            this.g.draw(
                    new Line2D.Double(lineX + rectangleWidth, lineY, lineX + rectangleWidth, lineY + rectangleHeight));
        }

        this.g.setStroke(orginalStroke);
    }

    public void drawHighLight(final int x, final int y, final int width, final int height, final boolean isRound) {
        final Paint originalPaint = this.g.getPaint();
        final Stroke originalStroke = this.g.getStroke();

        this.g.setPaint(CustomProcessDiagramCanvas.HIGHLIGHT_COLOR);
        this.g.setStroke(CustomProcessDiagramCanvas.THICK_TASK_BORDER_STROKE);

        // 判断是否为圆形
        if (isRound) {
            final Ellipse2D circle = new Ellipse2D.Double(x, y, width, height);
            this.g.draw(circle);
        } else {
            //RoundRectangle2D rect = new RoundRectangle2D.Double(x, y, width, height, 20, 20);
            final RoundRectangle2D rect = new RoundRectangle2D.Double(x, y, width, height, 30, 0);
            this.g.draw(rect);
        }

        this.g.setPaint(originalPaint);
        this.g.setStroke(originalStroke);
    }

    /**
     * Desc: 绘制正在执行中的节点红色高亮显示
     *
     * @param x
     * @param y
     * @param width
     * @param height
     * @author Fuxs
     * @param isRound
     */
    public void drawRunningActivitiHighLight(final int x, final int y, final int width, final int height,
            final boolean isRound) {
        final Paint originalPaint = this.g.getPaint();
        final Stroke originalStroke = this.g.getStroke();

        this.g.setPaint(CustomProcessDiagramCanvas.RUNNING_HIGHLIGHT_COLOR);
        this.g.setStroke(CustomProcessDiagramCanvas.THICK_TASK_BORDER_STROKE);

        // 判断是否为圆形
        if (isRound) {
            final Ellipse2D circle = new Ellipse2D.Double(x, y, width, height);
            this.g.draw(circle);
        } else {
            //todo 修改长方形弧度
            //RoundRectangle2D rect = new RoundRectangle2D.Double(x, y, width, height, 20, 20);
            final RoundRectangle2D rect = new RoundRectangle2D.Double(x, y, width, height, 0, 0);
            this.g.draw(rect);
        }

        this.g.setPaint(originalPaint);
        this.g.setStroke(originalStroke);
    }

    public void drawTextAnnotation(final String text, final GraphicInfo graphicInfo) {
        int x = (int) graphicInfo.getX();
        final int y = (int) graphicInfo.getY();
        final int width = (int) graphicInfo.getWidth();
        final int height = (int) graphicInfo.getHeight();

        final Font originalFont = this.g.getFont();
        final Stroke originalStroke = this.g.getStroke();

        this.g.setFont(CustomProcessDiagramCanvas.ANNOTATION_FONT);

        final Path2D path = new Path2D.Double();
        x += .5;
        final int lineLength = 18;
        path.moveTo(x + lineLength, y);
        path.lineTo(x, y);
        path.lineTo(x, y + height);
        path.lineTo(x + lineLength, y + height);

        path.lineTo(x + lineLength, (y + height) - 1);
        path.lineTo(x + 1, (y + height) - 1);
        path.lineTo(x + 1, y + 1);
        path.lineTo(x + lineLength, y + 1);
        path.closePath();

        this.g.draw(path);

        final int boxWidth = width - (2 * CustomProcessDiagramCanvas.ANNOTATION_TEXT_PADDING);
        final int boxHeight = height - (2 * CustomProcessDiagramCanvas.ANNOTATION_TEXT_PADDING);
        final int boxX = (x + (width / 2)) - (boxWidth / 2);
        final int boxY = (y + (height / 2)) - (boxHeight / 2);

        if ((text != null) && (text.isEmpty() == false)) {
            this.drawMultilineAnnotationText(text, boxX, boxY, boxWidth, boxHeight);
        }

        // restore originals
        this.g.setFont(originalFont);
        this.g.setStroke(originalStroke);
    }

    public void drawLabel(final String text, final GraphicInfo graphicInfo) {
        this.drawLabel(text, graphicInfo, true);
    }

    /**
     * 绘制流程线名称 Desc:
     *
     * @param text
     * @param graphicInfo
     * @param centered
     * @author Fuxs
     */
    public void drawLabel(final String text, final GraphicInfo graphicInfo, final boolean centered) {
        final float interline = 1.0f;

        // text
        if ((text != null) && (text.length() > 0)) {
            final Paint originalPaint = this.g.getPaint();
            final Font originalFont = this.g.getFont();

            this.g.setPaint(CustomProcessDiagramCanvas.LABEL_COLOR);
            this.g.setFont(CustomProcessDiagramCanvas.LABEL_FONT);

            final int wrapWidth = 100;
            int textY = (int) (graphicInfo.getY() + graphicInfo.getHeight());

            // TODO: use drawMultilineText()
            final AttributedString as = new AttributedString(text);
            as.addAttribute(TextAttribute.FOREGROUND, this.g.getPaint());
            as.addAttribute(TextAttribute.FONT, this.g.getFont());
            final AttributedCharacterIterator aci = as.getIterator();
            final FontRenderContext frc = new FontRenderContext(null, true, false);
            final LineBreakMeasurer lbm = new LineBreakMeasurer(aci, frc);

            while (lbm.getPosition() < text.length()) {
                final TextLayout tl = lbm.nextLayout(wrapWidth);
                textY += tl.getAscent();
                final Rectangle2D bb = tl.getBounds();
                double tX = graphicInfo.getX();
                if (centered) {
                    tX += (int) ((graphicInfo.getWidth() / 2) - (bb.getWidth() / 2));
                }
                tl.draw(this.g, (float) tX, textY);
                textY += tl.getDescent() + tl.getLeading() + ((interline - 1.0f) * tl.getAscent());
            }

            // restore originals
            this.g.setFont(originalFont);
            this.g.setPaint(originalPaint);
        }
    }

    /**
     * This method makes coordinates of connection flow better.
     *
     * @param sourceShapeType
     * @param targetShapeType
     * @param sourceGraphicInfo
     * @param targetGraphicInfo
     * @param graphicInfoList
     */
    public List<GraphicInfo> connectionPerfectionizer(final SHAPE_TYPE sourceShapeType,
            final SHAPE_TYPE targetShapeType, final GraphicInfo sourceGraphicInfo, final GraphicInfo targetGraphicInfo,
            final List<GraphicInfo> graphicInfoList) {
        final Shape shapeFirst = CustomProcessDiagramCanvas.createShape(sourceShapeType, sourceGraphicInfo);
        final Shape shapeLast = CustomProcessDiagramCanvas.createShape(targetShapeType, targetGraphicInfo);

        if ((graphicInfoList != null) && (graphicInfoList.size() > 0)) {
            final GraphicInfo graphicInfoFirst = graphicInfoList.get(0);
            final GraphicInfo graphicInfoLast = graphicInfoList.get(graphicInfoList.size() - 1);
            if (shapeFirst != null) {
                graphicInfoFirst.setX(shapeFirst.getBounds2D().getCenterX());
                graphicInfoFirst.setY(shapeFirst.getBounds2D().getCenterY());
            }
            if (shapeLast != null) {
                graphicInfoLast.setX(shapeLast.getBounds2D().getCenterX());
                graphicInfoLast.setY(shapeLast.getBounds2D().getCenterY());
            }

            Point p = null;

            if (shapeFirst != null) {
                final Line2D.Double lineFirst = new Line2D.Double(graphicInfoFirst.getX(), graphicInfoFirst.getY(),
                        graphicInfoList.get(1).getX(), graphicInfoList.get(1).getY());
                p = CustomProcessDiagramCanvas.getIntersection(shapeFirst, lineFirst);
                if (p != null) {
                    graphicInfoFirst.setX(p.getX());
                    graphicInfoFirst.setY(p.getY());
                }
            }

            if (shapeLast != null) {
                final Line2D.Double lineLast = new Line2D.Double(graphicInfoLast.getX(), graphicInfoLast.getY(),
                        graphicInfoList.get(graphicInfoList.size() - 2).getX(),
                        graphicInfoList.get(graphicInfoList.size() - 2).getY());
                p = CustomProcessDiagramCanvas.getIntersection(shapeLast, lineLast);
                if (p != null) {
                    graphicInfoLast.setX(p.getX());
                    graphicInfoLast.setY(p.getY());
                }
            }
        }

        return graphicInfoList;
    }

    /**
     * This method creates shape by type and coordinates.
     *
     * @param shapeType
     * @param graphicInfo
     * @return Shape
     */
    private static Shape createShape(final SHAPE_TYPE shapeType, final GraphicInfo graphicInfo) {
        if (SHAPE_TYPE.Rectangle.equals(shapeType)) {
            // source is rectangle
            return new Rectangle2D.Double(graphicInfo.getX(), graphicInfo.getY(), graphicInfo.getWidth(),
                    graphicInfo.getHeight());
        } else if (SHAPE_TYPE.Rhombus.equals(shapeType)) {
            // source is rhombus
            final Path2D.Double rhombus = new Path2D.Double();
            rhombus.moveTo(graphicInfo.getX(), graphicInfo.getY() + (graphicInfo.getHeight() / 2));
            rhombus.lineTo(graphicInfo.getX() + (graphicInfo.getWidth() / 2),
                    graphicInfo.getY() + graphicInfo.getHeight());
            rhombus.lineTo(graphicInfo.getX() + graphicInfo.getWidth(),
                    graphicInfo.getY() + (graphicInfo.getHeight() / 2));
            rhombus.lineTo(graphicInfo.getX() + (graphicInfo.getWidth() / 2), graphicInfo.getY());
            rhombus.lineTo(graphicInfo.getX(), graphicInfo.getY() + (graphicInfo.getHeight() / 2));
            rhombus.closePath();
            return rhombus;
        } else if (SHAPE_TYPE.Ellipse.equals(shapeType)) {
            // source is ellipse
            return new Ellipse2D.Double(graphicInfo.getX(), graphicInfo.getY(), graphicInfo.getWidth(),
                    graphicInfo.getHeight());
        } else {
            // unknown source element, just do not correct coordinates
        }
        return null;
    }

    /**
     * This method returns intersection point of shape border and line.
     *
     * @param shape
     * @param line
     * @return Point
     */
    private static Point getIntersection(final Shape shape, final Line2D.Double line) {
        if (shape instanceof Ellipse2D) {
            return CustomProcessDiagramCanvas.getEllipseIntersection(shape, line);
        } else if ((shape instanceof Rectangle2D) || (shape instanceof Path2D)) {
            return CustomProcessDiagramCanvas.getShapeIntersection(shape, line);
        } else {
            // something strange
            return null;
        }
    }

    /**
     * This method calculates ellipse intersection with line
     *
     * @param shape Bounds of this shape used to calculate parameters of
     *        inscribed into this bounds ellipse.
     * @param line
     * @return Intersection point
     */
    private static Point getEllipseIntersection(final Shape shape, final Line2D.Double line) {
        final double angle = Math.atan2(line.y2 - line.y1, line.x2 - line.x1);
        final double x = ((shape.getBounds2D().getWidth() / 2) * Math.cos(angle)) + shape.getBounds2D().getCenterX();
        final double y = ((shape.getBounds2D().getHeight() / 2) * Math.sin(angle)) + shape.getBounds2D().getCenterY();
        final Point p = new Point();
        p.setLocation(x, y);
        return p;
    }

    /**
     * This method calculates shape intersection with line.
     *
     * @param shape
     * @param line
     * @return Intersection point
     */
    private static Point getShapeIntersection(final Shape shape, final Line2D.Double line) {
        final PathIterator it = shape.getPathIterator(null);
        final double[] coords = new double[6];
        final double[] pos = new double[2];
        Line2D.Double l = new Line2D.Double();
        while (!it.isDone()) {
            final int type = it.currentSegment(coords);
            switch (type) {
            case PathIterator.SEG_MOVETO:
                pos[0] = coords[0];
                pos[1] = coords[1];
                break;
            case PathIterator.SEG_LINETO:
                l = new Line2D.Double(pos[0], pos[1], coords[0], coords[1]);
                if (line.intersectsLine(l)) {
                    return CustomProcessDiagramCanvas.getLinesIntersection(line, l);
                }
                pos[0] = coords[0];
                pos[1] = coords[1];
                break;
            case PathIterator.SEG_CLOSE:
                break;
            default:
                // whatever
            }
            it.next();
        }
        return null;
    }

    /**
     * This method calculates intersections of two lines.
     *
     * @param a Line 1
     * @param b Line 2
     * @return Intersection point
     */
    private static Point getLinesIntersection(final Line2D a, final Line2D b) {
        final double d = ((a.getX1() - a.getX2()) * (b.getY2() - b.getY1()))
                - ((a.getY1() - a.getY2()) * (b.getX2() - b.getX1()));
        final double da = ((a.getX1() - b.getX1()) * (b.getY2() - b.getY1()))
                - ((a.getY1() - b.getY1()) * (b.getX2() - b.getX1()));
        // double db = (a.getX1()-a.getX2())*(a.getY1()-b.getY1()) - (a.getY1()-a.getY2())*(a.getX1()-b.getX1());
        final double ta = da / d;
        // double tb = db/d;
        final Point p = new Point();
        p.setLocation(a.getX1() + (ta * (a.getX2() - a.getX1())), a.getY1() + (ta * (a.getY2() - a.getY1())));
        return p;
    }

    public void drawRevokeHighLight(final int x, final int y, final int width, final int height) {

        this.g.drawImage(CustomProcessDiagramCanvas.CANCEL_END_IMAGE,
                x + (width / 3),
                y + (height / 4),
                (CustomProcessDiagramCanvas.CANCEL_END_IMAGE.getWidth()),
                (CustomProcessDiagramCanvas.CANCEL_END_IMAGE.getHeight()), null);
    }
}
